package cn.dengta.context.web;

import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;

import cn.dengta.common.security.AbuseDefender;
import cn.dengta.common.util.SpringUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

/**
 * 方法级别的注释
 * eg：
 *
 * @AroundExecute( pre = @PreExecute(bean = UserIncomeDailyLogAdminService.class, frequency = 5, async = true)
 * post = @PreExecute(bean = UserIncomeDailyLogAdminService.class, frequency = 5, async = true)
 * )
 * public void m(){};
 * 在m()被调用前异步调用UserIncomeDailyLogAdminService.preExecute()
 * 在m()被调用后异步调用UserIncomeDailyLogAdminService.postExecute()
 * UserIncomeDailyLogAdminService 需要实现IPreExecute和IPostExecute接口
 */
@Slf4j
@Aspect
@Component
public class AroundExecuteInterceptor {

	private final ConcurrentHashMap<String, FrequencyLimitAbuseDefender<Class>> defenderMap = new ConcurrentHashMap<>(32);

	@Pointcut("@annotation(cn.dengta.context.web.AroundExecute)")
	public void methodPointcut() {
	}

	@Before("methodPointcut()")
	public void before(JoinPoint joinPoint) throws Exception {
		try {
			doBefore(joinPoint);
		} catch (Exception e) {
			log.error("AroundExecuteInterceptor.before error", e);
		}
	}

	@AfterReturning(returning = "ret", pointcut = "methodPointcut()")
	public void afterReturning(JoinPoint joinPoint, Object ret) throws Exception {
		try {
			doAfter(joinPoint);
		} catch (Exception e) {
			log.error("AroundExecuteInterceptor.afterReturning error", e);
		}
	}

	@AfterThrowing(pointcut = "methodPointcut()", throwing = "e")
	public void afterThrowing(JoinPoint joinPoint, Throwable e) throws Exception {
		try {
			doAfter(joinPoint);
		} catch (Exception ex) {
			log.error("AroundExecuteInterceptor.afterThrowing error", ex);
		}
	}

	private void doBefore(JoinPoint joinPoint) {
		Method method = ((MethodSignature) (joinPoint.getSignature())).getMethod();
		AroundExecute aroundExecute = method.getAnnotation(AroundExecute.class);
		if (aroundExecute == null) {
			return;
		}
		PreExecute[] preArr = aroundExecute.pre();
		if (ArrayUtils.isEmpty(preArr)) {
			return;
		}

		for (int i = 0; i < preArr.length; i++) {
			PreExecute preExecute = preArr[i];
			Class<? extends IPreExecute> preExecuteClass = preExecute.bean();
			if (preExecuteClass == null) {
				continue;
			}

			if (preExecute.frequency() > 0) {
				FrequencyLimitAbuseDefender<Class> defender = defenderMap.computeIfAbsent(getFullName(method) + "[" + i + "]", key -> new FrequencyLimitAbuseDefender<>(
						new AbuseDefender.Threshold(1, preExecute.frequency() * 1000L),
						c -> c.getCanonicalName() + "." + "preExecute"));
				if (!defender.assertResult(preExecuteClass)) {
					continue;
				}

			}
			IPreExecute bean = SpringUtil.getBean(preExecuteClass);
			if (preExecute.async()) {
				SpringUtil.asyncRun(bean::preExecute);
			} else {
				bean.preExecute();
			}
		}
	}

	private void doAfter(JoinPoint joinPoint) {
		Method method = ((MethodSignature) (joinPoint.getSignature())).getMethod();
		AroundExecute aroundExecute = method.getAnnotation(AroundExecute.class);
		if (aroundExecute == null) {
			return;
		}
		PostExecute[] postArr = aroundExecute.post();
		if (ArrayUtils.isEmpty(postArr)) {
			return;
		}
		for (int i = 0; i < postArr.length; i++) {
			PostExecute postExecute = postArr[i];
			Class<? extends IPostExecute> postExecuteClass = postExecute.bean();
			if (postExecuteClass == null) {
				continue;
			}
			if (postExecute.frequency() > 0) {
				FrequencyLimitAbuseDefender<Class> defender = defenderMap.computeIfAbsent(getFullName(method) + "[" + i + "]", key -> new FrequencyLimitAbuseDefender<>(
						new AbuseDefender.Threshold(1, postExecute.frequency() * 1000L),
						c -> c.getCanonicalName() + "." + "postExecute"));
				if (defender.execute(postExecuteClass) == AbuseDefender.State.NO) {
					continue;
				}

			}
			IPostExecute bean = SpringUtil.getBean(postExecuteClass);
			if (postExecute.async()) {
				SpringUtil.asyncRun(bean::postExecute);
			} else {
				bean.postExecute();
			}
		}
	}

	private String getFullName(Method method) {
		return method.getDeclaringClass().getCanonicalName() + "." + method.getName();
	}

}
